Skip to main content
Version: Current

Make your reference line interactive

In this example, we will show how you can change the reference line dynamically on any interaction.

Intro to Side effects

Side Effects are used to create or change any properties of elements in the visualization. For example, if we want to add a tooltip on hover, we add a tooltip side effect.

There are two types of side-effects in Muze which you need to extend to create a side-effect

TypeDescription
Surrogate Side EffectThese are the side effects which changes something in the visualization, for example, changing the color of any plots or data of any plots.
Spawnable Side EffectThese are the side effects which adds a new element in the visualization, for example, a tooltip or a selection box.

Create the side effect

For dyamically updating the reference line on interaction, first we need to create a side-effect.

Create a side effect class:

class AverageLine extends SurrogateSideEffect {
// Used to identify the side effect
static formalName() {
return "averageLine";
}
// Tells muze on which component the side effect will be applied.Here we have
// set the target as visual-unit. VisualUnit is the component where the layers
// are rendered.
static target() {
return "visual-unit";
}
}

Setting a formalName of the class is important as it will be used later to map the side effect to an interaction.

Register the side effect in ActionModel:

muze.ActionModel.for(canvas).registerSideEffects(AverageLine);

Update reference line data on interaction

Next, we implement the apply method. The apply method of side effect is the part which does the change to the visualization. Any code related to changing elements of visualization will go here.

Here is the code of apply method, which dynamically sets the data of the reference line layer when the interaction happens.

class AverageLine extends SurrogateSideEffect {
apply(selectionSet) {
const model = selectionSet.mergedEnter.model;
if (model) {
const groupedModel = model.groupBy(["Horsepower"]);
this.setLayerData(groupedModel, "averageLine");
const mountPoint = this.firebolt.context.layers()[1].mount();

const yAxes = this.firebolt.context.axes().y[0];
const selectedValues = groupedModel.getFieldData("Horsepower");
const averageValue =
selectedValues.reduce((acc, num) => acc + num, 0) /
selectedValues.length;

const yMark = yAxes.getScaleValue(averageValue);

const lineConfig = {
yPosition: yMark,
width: this.firebolt.context.width(),
stroke: "red",
strokeWidth: 2,
xOffset: yAxes.renderConfig().viewport.width,
};

// Clear the mount point contents
while (mountPoint.firstChild) {
mountPoint.removeChild(mountPoint.firstChild);
}

// Remove any existing average line overlays from document
const existingLines = document.querySelectorAll(".average-line-overlay");
existingLines.forEach((line) => line.remove());

// Find the SVG container first to ensure it exists
const svgContainer = document.querySelector("svg");
if (!svgContainer) {
throw new Error("SVG container not found");
}

// Create a new line element
const line = document.createElementNS(
"http://www.w3.org/2000/svg",
"path",
);

// Set line attributes using configuration
const startX = lineConfig.xOffset;
const endX = lineConfig.width + lineConfig.xOffset;
line.setAttribute(
"d",
`M${startX},${lineConfig.yPosition}L${endX},${lineConfig.yPosition}`,
);
line.setAttribute("stroke", lineConfig.stroke);
line.setAttribute("stroke-width", lineConfig.strokeWidth);
line.setAttribute("fill", "none");

// Create a group element for the line
const lineGroup = document.createElementNS(
"http://www.w3.org/2000/svg",
"g",
);
lineGroup.classList.add("average-line-overlay");
lineGroup.appendChild(line);

// Add the line group to the SVG container
svgContainer.appendChild(lineGroup);

// Position the line above other elements
lineGroup.style.zIndex = "1000";
} else {
this.resetLayerData("averageLine");
}
}
}

The apply method receives a selectionSet object in its parameter. selectionSet.entrySet.model and selectionSet.exitSet.model are both DataStore instances which are wrappers over DataModel instance with some utility functions created by Muze.

The SurrogateSideEffect class also provide access to firebolt context. This provides all the necessary information about the canvas, axes, and other chart information. For more details, refer to the Firebolt API reference.

We will briefly describe some of the important properties of this object which will be useful for creating the side effect.

SelectionSet

  1. entrySet: Contains the data and ids of the plots which are interacted with.
  2. uids: Set of entry ids to uniquely identify the plot.
  3. model: DataStore instance which has the data of the plots interacted with.
  4. exitSet: Contains the data and ids of the plots which are not interacted with.
  5. uids: Set of exit ids to uniquely identify the plot.

Finally, we map the side effect with a behavioural action (brush).

...
.config({
interaction: {
brush: {
sideEffects: {
"averageLine": {
enabled: true,
},
},
},
}
})
...

Complete code with output is shown below:

HTML

<div id="chart"></div>

JS

const DataModel = muze.DataModel;
const env = await muze();

const formattedData = await DataModel.loadData(data, schema);
let rootData = new DataModel(formattedData);
const SurrogateSideEffect = muze.SideEffects.standards.SurrogateSideEffect;
const html = muze.Operators.html;

const canvas = muze
.canvas()
.data(rootData)
.rows(["Horsepower"])
.columns(["Year"])
.config(
{
interaction: {
brush: {
sideEffects: {
"averageLine": {
enabled: true,
},
},
},
},
}
)
.layers([
{
mark: "bar",
},
{
mark: "tick",
name: "averageLine",
className: "averageLine",
encoding: {
x: {
field: null,
},
y: "Horsepower",
color: {
value: () => "#ff0000",
},
},
source: (dm) => {return dm.groupBy(["Horsepower"])},
},

])
.mount("#chart");

muze.ActionModel.for(canvas)
.registerSideEffects(
class AverageLine extends SurrogateSideEffect {
static formalName() {
return "averageLine";
}
apply(selectionSet) {
const model = selectionSet.entrySet.model;
if (model) {
const groupedModel = model.groupBy(["Horsepower"]);
this.setLayerData(groupedModel, "averageLine");
const mountPoint = this.firebolt.context.layers()[1].mount();
const yAxes = this.firebolt.context.axes().y[0];
const selectedValues = groupedModel.getFieldData("Horsepower");
const averageValue = (selectedValues.reduce((acc, num) => acc + num, 0))/selectedValues.length;

const yMark = yAxes.getScaleValue(averageValue);
// Configuration variables
const lineConfig = {
yPosition: yMark,
width: this.firebolt.context.width(),
stroke: "red",
strokeWidth: 2,
xOffset: yAxes.renderConfig().viewport.width
};

// Clear the mount point contents
while (mountPoint.firstChild) {
mountPoint.removeChild(mountPoint.firstChild);
}

// Remove any existing average line overlays from document
const existingLines = document.querySelectorAll('.average-line-overlay');
existingLines.forEach(line => line.remove());

// Find the SVG container first to ensure it exists
const svgContainer = document.querySelector('.muze-visual-unit');
if (!svgContainer) {
throw new Error('SVG container not found');
}

// Create a new line element
const line = document.createElementNS("http://www.w3.org/2000/svg", "path");

// Set line attributes using configuration
const startX = 0;
const endX = lineConfig.width + lineConfig.xOffset;
line.setAttribute("d", `M${startX},${lineConfig.yPosition}L${endX},${lineConfig.yPosition}`);
line.setAttribute("stroke", lineConfig.stroke);
line.setAttribute("stroke-width", lineConfig.strokeWidth);
line.setAttribute("fill", "none");

// Create a group element for the line
const lineGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
lineGroup.classList.add("average-line-overlay");
lineGroup.appendChild(line);

// Add the line group to the SVG container
svgContainer.appendChild(lineGroup);
} else {
this.resetLayerData("averageLine");
}
}
static target() {
return "visual-unit";
}
},
)

The output looks like this: